/*
 * Nest.js - nested structures construction library.
 * http://code.google.com/p/nest-js/
 * Copyright (c) 2011 Ruslan Matveev
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
var njs = (function() {
	
	'use strict';
	
	if (!('bind' in Function.prototype)) {
		Function.prototype.bind = function(owner) {
			var that = this;
			if (arguments.length <= 1) {
				return function() {
					return that.apply(owner, arguments);
				};
			} else {
				var args = Array.prototype.slice.call(arguments, 1);
				return function() {
					return that.apply(owner,
						arguments.length===0 ?
						args :
						args.concat(Array.prototype.slice.call(arguments))
					);
				};
			}
		};
	}
	
	if (!('indexOf' in Array.prototype)) {
		Array.prototype.indexOf = function(find, i) {
			if (i === undefined) i = 0;
			if (i < 0) i += this.length;
			if (i < 0) i = 0;
			for (var n = this.length; i < n; i++) {
				if (i in this && this[i] === find) {
					return i;
				}
			}
			return -1;
		}
	}
	
	/**
	 * List of the base types.
	 * @type {Array.<string>}
	 * @const
	 */
	var BASE_TYPES = [
		'undefined', 'string', 'number', 'boolean',
		'function', 'array', 'object'
	];
	
	/**
	 * List of the reserved words.
	 * Cannnot be used as a function names.
	 * @type {Array.<string>}
	 * @const
	 */
	var RESERVED_WORDS = [
		'break', 'else', 'new', 'var', 'case', 'finally',
		'return', 'void', 'catch', 'for', 'switch', 'while',
		'continue', 'function', 'this', 'with', 'default',
		'if', 'throw', 'delete', 'in', 'try', 'do', 'instanceof',
		'typeof', 'abstract', 'enum', 'int', 'short', 'boolean',
		'export', 'interface', 'static', 'byte', 'extends',
		'long', 'super', 'char', 'final', 'native', 'synchronized',
		'class', 'float', 'package', 'throws', 'const', 'goto',
		'private', 'transient', 'debugger', 'implements', 'protected',
		'volatile', 'double', 'import', 'public', 'null', 'true', 'false'
	];
	
	/**
	 * Returns true if the value is null.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is null, otherwise false.
	 */
	function isNull(value) {
		return (value === null);
	}
	
	/**
	 * Returns true if the value is undefined.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is undefined, otherwise false.
	 */
	function isUndefined(value) {
		return (typeof(value) == 'undefined');
	}
	
	/**
	 * Returns true if the value is number.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is number, otherwise false.
	 */
	function isNumber(value) {
		return (typeof(value) == 'number');
	}
	
	/**
	 * Returns true if the value is boolean.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is boolean, otherwise false.
	 */
	function isBoolean(value) {
		return (typeof(value) == 'boolean');
	}
	
	/**
	 * Returns true if the value is function.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is function, otherwise false.
	 */
	function isFunction(value) {
		return (typeof(value) == 'function');
	}
	
	/**
	 * Returns true if the value is string.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is string, otherwise false.
	 */
	function isString(value) {
		return (typeof(value) == 'string');
	}
	
	/**
	 * Returns true if the value is object.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is object, otherwise false.
	 */
	function isObject(value) {
		return (typeof(value) == 'object');
	}
	
	/**
	 * Returns true if the value is array.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value is array, otherwise false.
	 */
	function isArray(value) {
		if (!isObject(value)) return false;
		return (value.constructor.prototype == Array.prototype);
	}
	
	/**
	 * Returns true if the value type belongs to the one of the base types.
	 * @param {Variant} value Value to check.
	 * @return {boolean} Returns true if the value type belongs to the one of the base types.
	 */
	function isBaseTypeName(value) {
		return (BASE_TYPES.indexOf(value) != -1);
	}
	
	/**
	 * Returns value's base type (according to the base types list).
	 * @param {Variant} value Value to check.
	 * @return {string} Returns value's base type name.
	 */
	function getBaseType(value) {
		if (isArray(value)) return 'array';
		var type = typeof(value);
		return (isBaseTypeName(type) ? type : false);
	}
	
	/**
	 * Returns true if the value is valid JavaScript identifier.
	 * @param {Variant} value Value to check.
	 * @return {string} Returns true if the value is valid JavaScript identifier, otherwise false.
	 */
	function isIdentifier(value) {
		if (RESERVED_WORDS.indexOf(value) != -1) return false;
		return /^[a-zA-Z_$]+[0-9a-zA-Z_$]*$/.test(value);
	}
	
	/**
	 * Executes a provided callback once per array element or object property.
	 * @param {Object} object Array or object to process.
	 * @param {Function} callback Function to execute for each element or property.
	 * @param {Object} thisObject Object to use as this when executing callback.
	 */
	function forEach(object, callback, thisObject) {
		// check if the given object is an array
		if (isArray(object)) {
			// check if there is no built-in analogue
			if (!Array.prototype.forEach) {
				for (var c = 0, length = object.length; c < length; c++) {
					// execute callback for each element in array
					callback.call(thisObject, object[c], c, object);
				}
			} else {
				// use built-in Array.prototype.forEach
				object.forEach.call(object, callback, thisObject);
			}
		} else if (isObject(object) || isFunction(object)) {
			for (var key in object) {
				// execute callback for each object property
				callback.call(thisObject, object[key], key, object);
			}
		}
	}
	
	/**
	 * Retrieves the list of function arguments.
	 * @param {Function} functionObject Function object to process.
	 * @return {Array.<string>} Array of function arguments names.
	 */
	function getFunctionArguments(functionObject) {
		// check arguments
		if (!isFunction(functionObject)) return;
		// how many arguments do we have
		var functionArity = functionObject.length;
		// convert functionObject to string
		var functionValue = functionObject.toString();
		// extract function arguments
		var functionArguments = functionValue.split(')', 2).shift();
		functionArguments = functionArguments.split('(', 2).pop();
		// return arguments names array
		return functionArguments.split(/\s?,\s?/, functionArity);
	}
	
	/**
	 * Sets function arguments names.
	 * @param {Function} functionObject Function object to process.
	 * @param {Array.<string>} argumentsList Array with argument names.
	 * @return {Function} New function object with updated arguments list.
	 */
	function setFunctionArguments(functionObject, argumentsList) {
		// check arguments
		if (!isFunction(functionObject)) return;
		// set argumentsList to empty array in case if it's not set
		if (!isArray(argumentsList)) argumentsList = [];
		// convert functionObject to string
		var functionValue = functionObject.toString();
		// extract function body
		functionValue = functionValue.split('{').slice(1).join('{');
		functionValue = functionValue.split('}').slice(0, -1).join('}');
		// create and return new function with updated arguments list
		return new Function(argumentsList, functionValue);
	}
	
	/**
	 * Binds object methods to thisObject.
	 * @param {Object} object Object to process.
	 * @param {Object} thisObject Object to bind methods to.
	 * @return {Object} Object with each method binded to thisObject.
	 */
	function bindMethods(object, thisObject) {
		// keep the reference for the recursive calls
		var self = arguments.callee;
		// check if object is a function
		if (isFunction(object)) {
			// save own properties in the object
			var ownProps = {};
			forEach(object, function(value, key, object) {
				if (!Function.prototype[key]) {
					// save property to restore after binding
					ownProps[key] = object[key];
				}
			}, this);
			// bind function to thisObject
			object = object.bind(thisObject);
			// restore own properties
			forEach(ownProps, function(value, key, ownProps) {
				object[key] = self.call(this, ownProps[key], thisObject);
			}, this);
		} if (isObject(object)) {
			// call self for each property in the object
			forEach(object, function(value, key, object) {
				// bind object's property value
				object[key] = self.call(this, object[key], thisObject);
			}, this);
		}
		return object;
	}
	
	/**
	 * Evaluates functionObject in the contextObject context with thisObject as this.
	 * @param {Function} functionObject Function to evaluate.
	 * @param {Object} contextObject Object that will be used as evaluation context.
	 * @return {Object} thisObject Object that will be used as this in each function call.
	 */
	function evaluate(functionObject, contextObject, thisObject) {
		// initialize arguments
		var argumentNames = [];
		var argumentValues = [];
		// walk over the contextObject
		forEach(contextObject, function(argumentValue, argumentName, contextObject) {
			// bind context member to thisObject
			var argumentValue = bindMethods(argumentValue, thisObject);
			// push argument name
			argumentNames.push(argumentName);
			// push argument value
			argumentValues.push(argumentValue);
		}, this);
		// create evaluator function out of the original one
		var evaluator = setFunctionArguments(functionObject, argumentNames);
		// execute evaluator with thisObject as this
		evaluator.apply(thisObject, argumentValues);
		// return thisObject as the result
		return thisObject;
	}
	
	function Definition(rootContext) {
		var localContext = {};
		(function(source, target) {
			for (var elementName in source) {
				var elementData = source[elementName];
				if (isFunction(elementData)) {
					
					/*
					for (var key in elementData) {
						if (!Function.prototype[key]) {
							
							arguments.callee.call(
								this,
								source[elementName][key],
								target[elementName] = {}
							);
						}
					}
					*/
					
					if (!isBaseTypeName(elementName)) {
						target[elementName] = new function(rootContext, elementName, elementHandlers) {
							var elementHandler = elementHandlers[elementName];
							var resultFunction = function() {
								// trigger create event on referring element
								var elementData = elementHandler.call(this.rootElement, {
									// event type
									'type': 'create',
									// element name
									'name': elementName,
									// original arguments
									'data': Array.prototype.slice.call(arguments)
								});
								
								var appendLevel = 0;
								var rootElement = this.rootElement;
								var ownerElement = this.elementsList;
								var appendCallback = function() {
									var self = arguments.callee;
									var argumentsList = [];
									for (var c = 0, length = arguments.length; c < length; c++) {
										var argument = arguments[c];
										if (!isFunction(argument) || !argument.parentIndex || !argument.elementData) {
											argumentsList.push(function(rootElement) {
												var baseType = getBaseType(argument);
												if (!rootContext[baseType]) return argument;
												return rootContext[baseType].call(rootElement, {
													'type': 'create',
													'name': baseType,
													'data': [argument]
												});
											}(rootElement));
										} else {
											delete(ownerElement[argument.parentIndex]);
											argumentsList.push(argument.elementData);
										}
									}
									var elementData = elementHandler.call(self.elementData, {
										'type': 'arguments',
										'name': elementName,
										'level': appendLevel,
										'data': argumentsList
									});
									if (!isUndefined(elementData)) {
										self.elementData = elementData;
									}
									appendLevel++;
									return self;
								}
								appendCallback.elementData = elementData;
								appendCallback.parentIndex = ownerElement.length;
								ownerElement.push(elementData);
								return appendCallback.apply(elementData, arguments);
							}
							
							resultFunction.tab = function() {};
							return resultFunction;
							
						}(rootContext.elements, elementName, source);
					}
				} else arguments.callee.call(
					this,
					source[elementName],
					target[elementName] = {}
				);
			}
		})(rootContext.elements, localContext);
		
		this.create = function(definition) {
			var rootElement = rootContext.handler.call(-1, {'type': 'create'});
			forEach(evaluate(definition, localContext, {
				'rootElement': rootElement,
				'elementsList': []
			}).elementsList, function(element) {
				rootContext.handler.call(rootElement, {
					'type': 'append',
					'data': element
				});
			});
			return rootContext.handler.call(rootElement, {'type': 'result'});
		}
	}
	
	return {
		define: function(definition, handler) {
			// check if the definition is a function
			if (isUndefined(definition) || !isFunction(definition)) {
				throw ('njs.define: definition must be a function');
			}
			// check if the handler is a function
			if (isUndefined(handler) || !isFunction(handler)) {
				throw ('njs.define: handler must be a function');
			}
			// create new Definition instance
			return new Definition({'handler': handler, 'elements': evaluate(definition, {
				'define': function(elementNames, elementHandler) {
					// check if the elementHandler is a function
					if (isUndefined(elementHandler) || !isFunction(elementHandler)) {
						throw ('njs.define: handler must be a function');
					}
					// keep the reference for the recursive calls
					var self = arguments.callee;
					// call define for each item in the typeName, in case if it's an array
					if (isArray(elementNames)) {
						// copy arguments without the first one
						var args = Array.prototype.slice.call(arguments, 1);
						// walk over elementNames array
						forEach(elementNames, function(elementName) {
							// call self with updated arguments
							self.apply(this, [elementName].concat(args));
						}, this);
					} else if (isObject(elementNames)) {
						// copy arguments without the first one
						var args = Array.prototype.slice.call(arguments, 1);
						// walk over elementNames object
						for (var prefix in elementNames) {
							this[prefix] = {};
							// call self with updated arguments
							self.apply(this[prefix], [elementNames[prefix]].concat(args));
						}
					} else {
						// initialize element name
						var elementName = elementNames;
						// check if the element name is a string
						if (!isString(elementName)) throw ('njs.define: "' + elementName + '" is not a string');
						// check if the element name matches all base types
						if (elementName == '*') return self.apply(this,
							// call define with updated arguments
							[BASE_TYPES].concat(Array.prototype.slice.call(arguments, 1))
						);
						// initialize target object
						var target = this;
						// check if element name contains namespace prefix
						if (elementName.indexOf(':') != -1) {
							var namespace = elementName.split(':');
							elementName = namespace.pop();
							namespace = [namespace.join(':')];
							namespace.forEach(function(prefix) {
								// check if the prefix is valid identifier
								if (isBaseTypeName(prefix) || !isIdentifier(prefix)) {
									throw ('njs.define: "' + namespace.join(':') + '" is not valid prefix');
								}
								// check if the prefix overrides built - in property of object's prototype
								if (!isUndefined(Object.prototype[prefix])) {
									throw ('njs.define: "' + namespace.join(':') + '" cannot be used as a prefix');
								}
								// initialize namespace object
								if (!target[prefix]) target[prefix] = {};
								// switch to the new namespace
								target = target[prefix];
							}, this);
						}
						// check if the element name is valid identifier
						if (!isBaseTypeName(elementName) && !isIdentifier(elementName)) {
							throw ('njs.define: "' + elementName + '" is not valid element name');
						}
						// check if the element name overrides built - in property of object's prototype
						if (!isUndefined(Object.prototype[elementName])) {
							throw ('njs.define: "' + elementName + '" cannot be used as a element name');
						}
						// check if the element has been already defined
						if (target[elementName]) {
							throw ('njs.define: Cannot redefine element "' + elementName + '"');
						}
						// initialize element handler
						target[elementName] = elementHandler;
					}
				}
			}, {})});
		}
	}
})();